/*
 *  $Id: VobSubFileRipper.cpp 2585 2010-09-18 12:39:20Z xhmikosr $
 *
 *  (C) 2003-2006 Gabest
 *  (C) 2006-2010 see AUTHORS
 *
 *  This Program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2, or (at your option)
 *  any later version.
 *
 *  This Program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with GNU Make; see the file COPYING.  If not, write to
 *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
 *  http://www.gnu.org/copyleft/gpl.html
 *
 */

#include "stdafx.h"
#include "vobsubfileripper.h"
#include "../DeCSS/VobDec.h"
#include "../Subtitles/CCDecoder.h"

//
// CVobSubFileRipper
//

CVobSubFileRipper::CVobSubFileRipper()
	: CVobSubFile(NULL)
	, m_fThreadActive(false)
	, m_fBreakThread(false)
	, m_fIndexing(false)
{
	m_rd.Reset();
	CAMThread::Create();
}

CVobSubFileRipper::~CVobSubFileRipper()
{
	CAMThread::CallWorker(CMD_EXIT);
	CAMThread::Close();
}

STDMETHODIMP CVobSubFileRipper::NonDelegatingQueryInterface(REFIID riid, void** ppv)
{
	return
		QI(IVSFRipper)
		__super::NonDelegatingQueryInterface(riid, ppv);
}

void CVobSubFileRipper::Log(log_t type, LPCTSTR lpszFormat, ...)
{
	CAutoLock cAutoLock(&m_csCallback);
	if(!m_pCallback) return;

	TCHAR buff[1024];

	va_list args;
	va_start(args, lpszFormat);
	_vstprintf(buff, lpszFormat, args);
	va_end(args);

	CString msg;
	switch(type)
	{
	default:
	case LOG_INFO:
		msg = _T("");
		break;
	case LOG_WARNING:
		msg = _T("WARNING: ");
		break;
	case LOG_ERROR:
		msg = _T("ERROR: ");
		break;
	}

	msg += buff;

	m_pCallback->OnMessage(msg);
}

void CVobSubFileRipper::Progress(double progress)
{
	CAutoLock cAutoLock(&m_csCallback);
	if(!m_pCallback) return;

	m_pCallback->OnProgress(progress);
}

void CVobSubFileRipper::Finished(bool fSucceeded)
{
	CAutoLock cAutoLock(&m_csCallback);
	if(!m_pCallback) return;

	m_pCallback->OnFinished(fSucceeded);
}

#define ReadBEb(var) \
	f.Read(&((BYTE*)&var)[0], 1); \
 
#define ReadBEw(var) \
	f.Read(&((BYTE*)&var)[1], 1); \
	f.Read(&((BYTE*)&var)[0], 1); \
 
#define ReadBEdw(var) \
	f.Read(&((BYTE*)&var)[3], 1); \
	f.Read(&((BYTE*)&var)[2], 1); \
	f.Read(&((BYTE*)&var)[1], 1); \
	f.Read(&((BYTE*)&var)[0], 1); \
 
bool CVobSubFileRipper::LoadIfo(CString fn)
{
	CString str;

	CFileStatus status;
	if(!CFileGetStatus(fn, status) || !status.m_size)
	{
		Log(LOG_ERROR, _T("Invalid ifo"));
		return(false);
	}

	CFile f;
	if(!f.Open(fn, CFile::modeRead|CFile::typeBinary|CFile::shareDenyNone))
	{
		Log(LOG_ERROR, _T("Cannot open ifo"));
		return(false);
	}

	Log(LOG_INFO, _T("Opening ifo OK"));

	char hdr[13];
	f.Read(hdr, 12);
	hdr[12] = 0;
	if(strcmp(hdr, "DVDVIDEO-VTS"))
	{
		Log(LOG_ERROR, _T("Not a Video Title Set IFO file!"));
		return(false);
	}

	// lang ids

	f.Seek(0x254, CFile::begin);

	WORD ids[32];
	memset(ids, 0, sizeof(ids));

	int len = 0;
	ReadBEw(len);

	for(ptrdiff_t i = 0; i < len; i++)
	{
		f.Seek(2, CFile::current); // 01 00 ?
		ReadBEw(ids[i]);
		if(ids[i] == 0) ids[i] = '--';
		f.Seek(2, CFile::current); // 00 00 ?
	}

	/* Video info */

	f.Seek(0x200, CFile::begin);
	f.Read(&m_rd.vidinfo, 2);

	SIZE res[4][2] =
	{
		{{720,480},{720,576}},
		{{704,480},{704,576}},
		{{352,480},{352,576}},
		{{352,240},{352,288}}
	};

	m_rd.vidsize = res[m_rd.vidinfo.source_res][m_rd.vidinfo.system&1];

	double rate = (m_rd.vidinfo.system == 0) ? 30.0/29.97 : 1.0;

	/* PGCs */

	{
		DWORD offset;

		DWORD pgcpos;
		f.Seek(0xc0+0x0c, CFile::begin);
		ReadBEdw(pgcpos);
		pgcpos *= 0x800;

		WORD nPGC;
		f.Seek(pgcpos, CFile::begin);
		ReadBEw(nPGC);

		m_rd.pgcs.RemoveAll();
		m_rd.pgcs.SetCount(nPGC);

		for(ptrdiff_t i = 0; i < nPGC; i++)
		{
			PGC& pgc = m_rd.pgcs[i];

			f.Seek(pgcpos + 8 + i*8 + 4, CFile::begin);
			ReadBEdw(offset);
			offset += pgcpos;

			BYTE nProgs, nCells;
			f.Seek(offset + 2, CFile::begin);
			ReadBEb(nProgs);
			ReadBEb(nCells);

			//

			memcpy(pgc.ids, ids, sizeof(ids));

			struct splanginfo {
				BYTE res1, id1, id2, res2;
			};
			splanginfo splinfo[32];

			f.Seek(offset + 0x1c, CFile::begin);
			f.Read(splinfo, 32*4);

			for(ptrdiff_t j = 0; j < 32; j++)
			{
				if(splinfo[j].id1 || splinfo[i].id2)
				{
					WORD tmpids[32];
					memset(tmpids, 0, sizeof(tmpids));

					for(j = 0; j < 32; j++)
					{
						if(!(splinfo[j].res1 & 0x80)) break;

						pgc.ids[splinfo[j].id1] = ids[j];
						pgc.ids[splinfo[j].id2] = ids[j];
					}

					break;
				}
			}

			//

			f.Seek(offset + 0xa4, CFile::begin);

			for(ptrdiff_t j = 0; j < 16; j++)
			{
				BYTE y, u, v, tmp;

				f.Read(&tmp, 1);
				f.Read(&y, 1);
				f.Read(&u, 1);
				f.Read(&v, 1);

				y = (y-16)*255/219;

				pgc.pal[j].rgbRed = (BYTE)min(max(1.0*y + 1.4022*(u-128), 0), 255);
				pgc.pal[j].rgbGreen = (BYTE)min(max(1.0*y - 0.3456*(u-128) - 0.7145*(v-128), 0), 255);
				pgc.pal[j].rgbBlue = (BYTE)min(max(1.0*y + 1.7710*(v-128), 0) , 255);
			}

			//

			WORD progoff, celladdroff, vobcelloff;
			f.Seek(offset + 0xe6, CFile::begin);
			ReadBEw(progoff);
			f.Seek(offset + 0xe8, CFile::begin);
			ReadBEw(celladdroff);
			f.Seek(offset + 0xea, CFile::begin);
			ReadBEw(vobcelloff);

			//

			CAtlArray<BYTE> progs;
			progs.SetCount(nProgs);
			f.Seek(offset + progoff, CFile::begin);
			f.Read(progs.GetData(), nProgs);

			//

			pgc.angles[0].SetCount(nCells);
			pgc.iSelAngle = 0;

			//

			f.Seek(offset + vobcelloff, CFile::begin);
			for(ptrdiff_t j = 0; j < nCells; j++)
			{
				ReadBEw(pgc.angles[0][j].vob);
				ReadBEw(pgc.angles[0][j].cell);
			}

			//

			DWORD tOffset = 0, tTotal = 0;

			int iAngle = 0;

			pgc.nAngles = 0;

			f.Seek(offset + celladdroff, CFile::begin);
			for(ptrdiff_t j = 0; j < nCells; j++)
			{
				BYTE b;
				ReadBEb(b);
				switch(b>>6)
				{
				case 0:
					iAngle = 0;
					break; // normal
				case 1:
					iAngle = 1;
					break; // first angle block
				case 2:
					iAngle++;
					break; // middle angle block
				case 3:
					iAngle++;
					break; // last angle block (no more should follow)
				}
				pgc.angles[0][j].iAngle = iAngle;
				pgc.nAngles = max(pgc.nAngles, iAngle);

				f.Seek(3, CFile::current);
				ReadBEdw(pgc.angles[0][j].tTime);
				ReadBEdw(pgc.angles[0][j].start);
				f.Seek(8, CFile::current);
				ReadBEdw(pgc.angles[0][j].end);

				float fps;
				switch((pgc.angles[0][j].tTime>>6)&0x3)
				{
				default:
				case 3:
					fps = 30;
					break;
				case 1:
					fps = 25;
					break;
				}

				int t = pgc.angles[0][j].tTime;
				int hh = ((t>>28)&0xf)*10+((t>>24)&0xf);
				int mm = ((t>>20)&0xf)*10+((t>>16)&0xf);
				int ss = ((t>>12)&0xf)*10+((t>>8)&0xf);
				int ms = (int)(1000.0 * (((t>>4)&0x3)*10+((t>>0)&0xf)) / fps);
				pgc.angles[0][j].tTime = (DWORD)((((hh*60+mm)*60+ss)*1000+ms)*rate);

				// time discontinuity
				if(b&0x02) tOffset = tTotal;
				pgc.angles[0][j].fDiscontinuity = !!(b&0x02);

				pgc.angles[0][j].tTotal = tTotal;
				pgc.angles[0][j].tOffset = tOffset;

				tTotal += pgc.angles[0][j].tTime;
			}

			for(iAngle = 1; iAngle <= 9; iAngle++)
			{
				tOffset = tTotal = 0;

				for(ptrdiff_t j = 0, k = 0; j < nCells; j++)
				{
					if(pgc.angles[0][j].iAngle != 0
					&& pgc.angles[0][j].iAngle != iAngle)
						continue;

					pgc.angles[iAngle].Add(pgc.angles[0][j]);

					if(pgc.angles[iAngle][k].fDiscontinuity) tOffset = tTotal;

					pgc.angles[iAngle][k].tTotal = tTotal;
					pgc.angles[iAngle][k].tOffset = tOffset;

					tTotal += pgc.angles[iAngle][k].tTime;

					k++;
				}
			}
		}
	}

	Log(LOG_INFO, _T("Parsing ifo OK"));

	return(true);
}

bool CVobSubFileRipper::LoadVob(CString fn)
{
	Log(LOG_INFO, _T("Searching vobs..."));
/*
	CAtlList<CString> m_vobs;

	fn = fn.Left(fn.ReverseFind('.')+1);
	fn.TrimRight(_T(".0123456789"));
	for(ptrdiff_t i = 0; i < 100; i++)
	{
		CString vob;
		vob.Format(_T("%s%d.vob"), fn, i);

		CFileStatus status;
		if(!(CFileGetStatus(vob, status) && status.m_size))
		{
			if(i > 0) break;
			else continue;
		}

		if(status.m_size&0x7ff)
		{
			Log(LOG_ERROR, _T("Length of %s is not n*2048!"), vob);
			m_vobs.RemoveAll();
			break;
		}

		CString str = _T("Found ") + vob;

		if(i == 0) 
		{
			str += _T(" (skipping, if not a menu vob rename it)");
		}
		else
		{
			m_vobs.AddTail(vob);
		}

		Log(LOG_INFO, str);
	}

	if(m_vobs.GetCount() <= 0)
	{
		Log(LOG_ERROR, _T("Nothing found! (%s*.vob)"), fn);
		return(false);
	}
*/
	CAtlList<CString> vobs;
	if(!m_vob.Open(fn, vobs/*m_vobs*/))
	{
		Log(LOG_ERROR, _T("Cannot open vob sequence"));
		return(false);
	}

	if(vobs.GetCount() <= 0)
	{
		Log(LOG_ERROR, _T("Nothing found! (%s*.vob)"), fn);
		return(false);
	}

	POSITION pos = vobs.GetHeadPosition();
	while(pos) Log(LOG_INFO, _T("Found ") + vobs.GetNext(pos));

	if(m_vob.IsDVD())
	{
		Log(LOG_INFO, _T("DVD detected..."));

		BYTE key[5];

		if(m_vob.HasDiscKey(key))
			Log(LOG_INFO, _T("Disc key: %02x%02x%02x%02x%02x"), key[0], key[1], key[2], key[3], key[4]);
		else
			Log(LOG_WARNING, _T("Couldn't get the disc key"));

		if(m_vob.HasTitleKey(key))
			Log(LOG_INFO, _T("Title key: %02x%02x%02x%02x%02x"), key[0], key[1], key[2], key[3], key[4]);
		else
			Log(LOG_WARNING, _T("Couldn't get the title key"));

		BYTE buff[2048];

		m_vob.Seek(0);
		if(!m_vob.Read(buff))
		{
			Log(LOG_ERROR, _T("Can't read vob, please unlock it with a software player!"));
			return(false);
		}
		m_vob.Seek(0);
	}

	return(true);
}

DWORD CVobSubFileRipper::ThreadProc()
{
	SetThreadPriority(m_hThread, THREAD_PRIORITY_BELOW_NORMAL);

	while(1)
	{
		DWORD cmd = GetRequest();

		m_fThreadActive = true;

		switch(cmd)
		{
		case CMD_EXIT:
			Reply(S_OK);
			return 0;

		case CMD_INDEX:
			Reply(S_OK);
			{
				m_fIndexing = true;
				bool fSucceeded = Create();
				m_fIndexing = false;
				Finished(fSucceeded);
			}
			break;

		default:
			Reply((DWORD)E_FAIL);
			return (DWORD)-1;
		}

		m_fBreakThread = false;
		m_fThreadActive = false;
	}

	return 1;
}

static int SubPosSortProc(const void* e1, const void* e2)
{
	return((int)(((CVobSubFile::SubPos*)e1)->start - ((CVobSubFile::SubPos*)e2)->start));
}

bool CVobSubFileRipper::Create()
{
	CAutoLock cAutoLock(&m_csAccessLock);

	if(m_rd.iSelPGC < 0 || m_rd.iSelPGC >= m_rd.pgcs.GetCount())
	{
		Log(LOG_ERROR, _T("Invalid program chain number (%d)!"), m_rd.iSelPGC);
		return(false);
	}

	PGC& pgc = m_rd.pgcs[m_rd.iSelPGC];

	if(pgc.iSelAngle < 0 || pgc.iSelAngle > 9 || pgc.angles[pgc.iSelAngle].GetCount() == 0)
	{
		Log(LOG_ERROR, _T("Invalid angle number (%d)!"), pgc.iSelAngle);
		return(false);
	}

	CAtlArray<vc_t>& angle = pgc.angles[pgc.iSelAngle];

	if(m_rd.selids.GetCount() == 0 && !m_rd.fClosedCaption)
	{
		Log(LOG_ERROR, _T("No valid stream set to be extacted!"));
		return(false);
	}

	if(m_rd.selvcs.GetCount() == 0)
	{
		Log(LOG_ERROR, _T("No valid vob/cell id set to be extacted!"));
		return(false);
	}

	Log(LOG_INFO, _T("Indexing..."));

	// initalize CVobSubFile
	CVobSubFile::Close();
	InitSettings();
	m_title = m_outfn;
	m_size = m_rd.vidsize;
	TrimExtension(m_title);
	memcpy(m_orgpal, pgc.pal, sizeof(m_orgpal));
	m_sub.SetLength(0);

	CCDecoder ccdec(m_title + _T(".cc.srt"), m_title + _T(".cc.raw"));

	CVobDec vd;

	__int64 SCR, PTS = 0, tOffset = 0, tPrevOffset = 0, tTotal = 0, tStart = 0;
	int vob = 0, cell = 0;
	bool fDiscontinuity = false, fDiscontinuityFixApplied = false, fNavpackFound = false;

	int PTSframeoffset = 0, minPTSframeoffset = 0;

	if(m_rd.fResetTime)
	{
		for(size_t i = 0; i < angle.GetCount() && (UINT)((angle[i].vob<<16)|angle[i].cell) != m_rd.selvcs[0]; i++)
			tStart += angle[i].tTime;

		Log(LOG_INFO, _T("Counting timestamps from %I64dms (v%02dc%02d)"),
			tStart, m_rd.selvcs[0]>>16, m_rd.selvcs[0]&0xffff);
	}

	CAtlMap<DWORD, int> selvcmap;
	selvcmap.RemoveAll();
	for(ptrdiff_t i = 0; i < m_rd.selvcs.GetCount(); i++)
		selvcmap[m_rd.selvcs[i]] = 90000;

	CAtlArray<vcchunk> chunks, foundchunks, loadedchunks;

	if(m_vob.IsDVD())
	{
		Log(LOG_INFO, _T("Indexing mode: DVD"));

		for(ptrdiff_t i = 0; i < angle.GetCount(); i++)
		{
			DWORD vc = (angle[i].vob<<16)|angle[i].cell;
			if(!selvcmap.Lookup(vc))
				continue;

			vcchunk c = {2048i64*angle[i].start, 2048i64*angle[i].end+2048, vc};
			chunks.Add(c);

			Log(LOG_INFO, _T("Adding: 0x%x - 0x%x (lba) for vob %d cell %d"),
				angle[i].start, angle[i].end, angle[i].vob, angle[i].cell);
		}
	}
	else if(LoadChunks(loadedchunks))
	{
		Log(LOG_INFO, _T("Indexing mode: File"));

		for(ptrdiff_t i = 0; i < loadedchunks.GetCount(); i++)
		{
			DWORD vcid = loadedchunks[i].vc;
			if(!selvcmap.Lookup(vcid))
				continue;

			chunks.Add(loadedchunks[i]);
		}

		Log(LOG_INFO, _T(".chunk file loaded"));
	}
	else
	{
		Log(LOG_INFO, _T("Indexing mode: File"));

		chunks.RemoveAll();
		vcchunk c = {0, 2048i64*m_vob.GetLength(), 0};
		chunks.Add(c);
	}

	__int64 sizedone = 0, sizetotal = 0;
	for(ptrdiff_t i = 0; i < chunks.GetCount(); i++)
		sizetotal += chunks[i].end - chunks[i].start;

	for(ptrdiff_t i = 0; !m_fBreakThread && i < chunks.GetCount(); i++)
	{
		__int64 curpos = chunks[i].start, endpos = chunks[i].end;

		vcchunk curchunk = {curpos, curpos, chunks[i].vc};

		for(m_vob.Seek((int)(curpos/2048)); !m_fBreakThread && curpos < endpos; curpos += 2048, sizedone += 2048)
		{
			if(!(curpos&0x7ffff))
				Progress(1.0 * sizedone / sizetotal);

			static BYTE buff[2048];

			if(!m_vob.Read(buff))
			{
				Log(LOG_ERROR, _T("Cannot read, either locked dvd or truncated/missing files!"));
				return(false);
			}

			curchunk.end = curpos;

			if(buff[0x14] & 0x30)
			{
				if(!vd.m_fFoundKey)
				{
					Log(LOG_INFO, _T("Encrypted sector found, searching key..."));

					__int64 savepos = curpos;

					m_vob.Seek(0);
					for(__int64 pos = 0; !m_fBreakThread && pos < endpos; pos += 2048)
					{
						if(!m_vob.Read(buff))
						{
							Log(LOG_ERROR, _T("Cannot read, either locked dvd or truncated/missing files!"));
							return(false);
						}

						if(vd.FindKey(buff))
							break;
					}

					if(m_fBreakThread)
						break;

					if(!vd.m_fFoundKey)
					{
						Log(LOG_ERROR, _T("Key not found, can't decrypt!"));
						return(false);
					}

					Log(LOG_INFO, _T("Key found, continuing extraction..."));

					m_vob.Seek((int)((curpos = savepos)/2048));
					m_vob.Read(buff);
				}

				vd.Decrypt(buff);
			}

			if(*((DWORD*)&buff[0]) != 0xba010000)
			{
				Log(LOG_WARNING, _T("Bad sector header at block %08d!"), (int)(curpos/2048));

				if(AfxMessageBox(_T("Bad packet header found, do you want to continue?"), MB_YESNO) == IDNO)
				{
					Log(LOG_ERROR, _T("Terminated!"));
					return(false);
				}
			}

			SCR = (__int64(buff[0x04] & 0x38) >> 3) << 30
				  | __int64(buff[0x04] & 0x03) << 28
				  | __int64(buff[0x05]) << 20
				  | (__int64(buff[0x06] & 0xf8) >> 3) << 15
				  | __int64(buff[0x06] & 0x03) << 13
				  | __int64(buff[0x07]) << 5
				  | (__int64(buff[0x08] & 0xf8) >> 3) << 0;

			bool hasPTS = false;

			if((*(DWORD*)&buff[0x0e] == 0xe0010000 || *(DWORD*)&buff[0x0e] == 0xbd010000)
					&& buff[0x15] & 0x80)
			{
				PTS = (__int64)(buff[0x17] & 0x0e) << 29			// 32-30 (+marker)
					  | ((__int64)(buff[0x18]) << 22)				// 29-22
					  | ((__int64)(buff[0x19] & 0xfe) << 14)		// 21-15 (+marker)
					  | ((__int64)(buff[0x1a]) << 7)				// 14-07
					  | ((__int64)(buff[0x1b]) >> 1);				// 06-00 (+marker)

				hasPTS = true;
			}

			if(*((DWORD*)&buff[0x0e]) == 0xbb010000)
			{
				fNavpackFound = true;

				if(vob == buff[0x420] && cell == buff[0x422])
					continue;

				vob = buff[0x420];
				cell = buff[0x422];

				tOffset = tTotal = 0;

				for(ptrdiff_t i = 0; i < angle.GetCount(); i++)
				{
					if(angle[i].vob == vob && angle[i].cell == cell)
					{
						tPrevOffset = tOffset;
						tOffset = (__int64)angle[i].tOffset;
						tTotal = (__int64)angle[i].tTotal;
						fDiscontinuity = angle[i].fDiscontinuity;
						fDiscontinuityFixApplied = false;
						break;
					}
				}

				if(curchunk.vc != (DWORD)((vob<<16)|cell))
				{
					if(curchunk.vc != 0) foundchunks.Add(curchunk);
					curchunk.start = curchunk.end = curpos;
					curchunk.vc = (vob<<16)|cell;
				}

				CString str, str2;
				str.Format(_T("v%02d c%02d lba%08d"), vob, cell, (int)(curpos/2048));
				UINT vcid = (vob<<16)|cell;
				if(!selvcmap.Lookup(vcid, minPTSframeoffset)) str2 = _T(", skipping");
				else str2.Format(_T(", total=%I64dms, off=%I64dms, corr=%I64dms, discont.:%d"),
									 tTotal, tOffset, -tStart, (int)fDiscontinuity);
				Log(LOG_INFO, str + str2);
			}

			DWORD vcid = (vob<<16)|cell;
			if(!selvcmap.Lookup(vcid, minPTSframeoffset))
				continue;

			if(hasPTS && fDiscontinuity && !fDiscontinuityFixApplied)
			{
				__int64 tDiff = tOffset - tPrevOffset;
				if(tDiff > 0 && tDiff < (PTS/90+1000))
				{
					CString str;
					str.Format(_T("False discontinuity detected, correcting time by %I64dms"), -tDiff);
					Log(LOG_INFO, str);

					tStart += tDiff;
				}
				fDiscontinuityFixApplied = true;
			}

			if(*(DWORD*)&buff[0x0e] == 0xe0010000)
			{
				if(fDiscontinuity)
				{
					if(PTS < minPTSframeoffset)
					{
						selvcmap[vcid] = PTSframeoffset = PTS;
					}

					fDiscontinuity = false;
				}

				if(m_rd.fClosedCaption)
					ccdec.ExtractCC(buff, 2048, tOffset + ((PTS - PTSframeoffset) / 90) - tStart);
			}
			else if(*(DWORD*)&buff[0x0e] == 0xbd010000)
			{
				BYTE id = buff[0x17 + buff[0x16]], iLang = id&0x1f;

				if((id & 0xe0) == 0x20 && m_rd.selids.Lookup(iLang))
				{
					if(hasPTS)
					{
						SubPos sb;
						sb.filepos = m_sub.GetPosition();
						sb.start = tOffset + ((PTS - PTSframeoffset) / 90) - tStart;
						sb.vobid = (char)vob;
						sb.cellid = (char)cell;
						sb.celltimestamp = tTotal;
						sb.fValid = true;
						m_langs[iLang].subpos.Add(sb);
					}

					m_sub.Write(buff, 2048);
				}
			}
		}

		if(curchunk.vc != (DWORD)((vob<<16)|cell))
		{
			if(curchunk.vc != 0) foundchunks.Add(curchunk);
			curchunk.start = curchunk.end = curpos;
			curchunk.vc = (vob<<16)|cell;
		}
	}

	if(sizedone < sizetotal)
	{
		Log(LOG_ERROR, _T("Indexing terminated before reaching the end!"));
		Progress(0);
		return(false);
	}

	if(!fNavpackFound)
	{
		Log(LOG_ERROR, _T("Could not find any system header start code! (0x000001bb)"));
		if(!m_vob.IsDVD()) Log(LOG_ERROR, _T("Make sure the ripper doesn't strip these packets."));
		Progress(0);
		return(false);
	}

	Log(LOG_INFO, _T("Indexing finished"));
	Progress(1);

	for(ptrdiff_t i = 0; i < 32; i++)
	{
		if(m_iLang == -1 && m_langs[i].subpos.GetCount() > 0) m_iLang = i;
		m_langs[i].id = pgc.ids[i];
		m_langs[i].name = m_langs[i].alt = FindLangFromId(m_langs[i].id);

		CAtlArray<SubPos>& sp = m_langs[i].subpos;
		qsort(sp.GetData(), sp.GetCount(), sizeof(SubPos), SubPosSortProc);

		if(m_rd.fForcedOnly)
		{
			Log(LOG_INFO, _T("Searching for forced subs..."));
			Progress(0);

			for(ptrdiff_t j = 0, len = sp.GetCount(); j < len; j++)
			{
				Progress(1.0 * j / len);

				sp[j].fValid = false;
				int packetsize = 0, datasize = 0;
				if(BYTE* buff = GetPacket(j, packetsize, datasize, i))
				{
					m_img.GetPacketInfo(buff, packetsize, datasize);
					sp[j].fValid = m_img.fForced;
					delete [] buff;
				}
			}

			Progress(1);
		}
	}

	Log(LOG_INFO, _T("Saving files..."));

	if(m_iLang != -1)
	{
		if(!Save(m_title))
		{
			Log(LOG_ERROR, _T("Could not save output files!"));
			return(false);
		}
	}

	Log(LOG_INFO, _T("Subtitles saved"));

	if(!m_vob.IsDVD() && loadedchunks.GetCount() == 0)
	{
		if(SaveChunks(foundchunks))
		{
			Log(LOG_INFO, _T(".chunk file saved"));
		}
	}

	Log(LOG_INFO, _T("Done!"));

	return(true);
}

static const DWORD s_version = 1;

bool CVobSubFileRipper::LoadChunks(CAtlArray<vcchunk>& chunks)
{
	CFile f;

	CString fn = m_infn;
	TrimExtension(fn);
	fn += _T(".chunks");

	DWORD chksum = 0, chunklen, version;
	__int64 voblen = 0;

	if(!f.Open(fn, CFile::modeRead|CFile::typeBinary|CFile::shareDenyNone))
		return(false);
	f.Read(&version, sizeof(version));
	if(version == 1)
	{
		f.Read(&chksum, sizeof(chksum));
		f.Read(&voblen, sizeof(voblen));
		f.Read(&chunklen, sizeof(chunklen));
		chunks.SetCount(chunklen);
		f.Read(chunks.GetData(), sizeof(vcchunk)*chunks.GetCount());
	}
	f.Close();

	if(voblen != m_vob.GetLength())
	{
		chunks.RemoveAll();
		return(false);
	}

	if(!f.Open(m_infn, CFile::modeRead|CFile::typeBinary|CFile::shareDenyNone))
		return(false);
	DWORD dw, chksum2 = 0;
	while(f.Read(&dw, sizeof(dw)) == sizeof(dw)) chksum2 += dw;
	f.Close();

	if(chksum != chksum2)
	{
		chunks.RemoveAll();
		return(false);
	}

	return(true);
}

bool CVobSubFileRipper::SaveChunks(CAtlArray<vcchunk>& chunks)
{
	CFile f;

	CString fn = m_infn;
	TrimExtension(fn);
	fn += _T(".chunks");

	DWORD chksum = 0, chunklen = chunks.GetCount();
	__int64 voblen = m_vob.GetLength();

	if(!f.Open(m_infn, CFile::modeRead|CFile::typeBinary|CFile::shareDenyNone))
		return(false);
	DWORD dw;
	while(f.Read(&dw, sizeof(dw)) == sizeof(dw)) chksum += dw;
	f.Close();

	if(!f.Open(fn, CFile::modeCreate|CFile::modeWrite|CFile::typeBinary|CFile::shareDenyWrite))
		return(false);
	f.Write(&s_version, sizeof(s_version));
	f.Write(&chksum, sizeof(chksum));
	f.Write(&voblen, sizeof(voblen));
	f.Write(&chunklen, sizeof(chunklen));
	f.Write(chunks.GetData(), sizeof(vcchunk)*chunklen);
	f.Close();

	return(true);
}

// IVSFRipper

STDMETHODIMP CVobSubFileRipper::SetCallBack(IVSFRipperCallback* pCallback)
{
	CAutoLock cAutoLock(&m_csCallback);
	m_pCallback = pCallback;
	return S_OK;
}

STDMETHODIMP CVobSubFileRipper::LoadParamFile(CString fn)
{
	CAutoLock cAutoLock(&m_csAccessLock);

	m_rd.Reset();

	CStdioFile f;
	if(!f.Open(fn, CFile::modeRead|CFile::typeText))
		return E_FAIL;

	TCHAR langid[256];

	enum {P_INPUT, P_OUTPUT, P_PGC, P_ANGLE, P_LANGS, P_OPTIONS};
	int phase = P_INPUT;

	CString line;
	while(f.ReadString(line))
	{
		if(line.Trim().IsEmpty() || line[0] == '#') continue;

		if(phase == P_INPUT)
		{
			if(S_OK != SetInput(line)) break;
			phase = P_OUTPUT;
		}
		else if(phase == P_OUTPUT)
		{
			if(S_OK != SetOutput(line)) break;
			phase = P_PGC;
		}
		else if(phase == P_PGC)
		{
			m_rd.iSelPGC = _tcstol(line, NULL, 10)-1;
			if(m_rd.iSelPGC < 0 || m_rd.iSelPGC >= m_rd.pgcs.GetCount()) break;
			phase = P_ANGLE;
		}
		else if(phase == 3)
		{
			PGC& pgc = m_rd.pgcs[m_rd.iSelPGC];

			pgc.iSelAngle = _tcstol(line, NULL, 10);
			if(pgc.iSelAngle < 0 || pgc.iSelAngle > max(1, pgc.nAngles) || pgc.iSelAngle > 9) break;

			CAtlArray<vc_t>& angle = pgc.angles[pgc.iSelAngle];

			if(line.Find('v') >= 0)
			{
				int vob = 0, cell = 0;

				line += ' ';

				TCHAR* s = (LPTSTR)(LPCTSTR)line;
				TCHAR* e = s + line.GetLength();
				while(s < e)
				{
					if(*s == 'v' || s == e-1)
					{
						s++;
						if(vob != 0 && cell == 0)
						{
							for(ptrdiff_t i = 0; i < angle.GetCount(); i++)
							{
								if(angle[i].vob == vob)
									m_rd.selvcs.Add((angle[i].vob<<16)|angle[i].cell);
							}
						}

						vob = _tcstol(s, &s, 10);
						cell = 0;
					}
					else if(*s == 'c' && vob > 0)
					{
						s++;
						cell = _tcstol(s, &s, 10);

						for(ptrdiff_t i = 0; i < angle.GetCount(); i++)
						{
							if(angle[i].vob == vob && angle[i].cell == cell)
							{
								m_rd.selvcs.Add((vob<<16)|cell);
								break;
							}
						}
					}
					else
					{
						s++;
					}
				}
			}
			else
			{
				for(ptrdiff_t i = 0; i < angle.GetCount(); i++)
					m_rd.selvcs.Add((angle[i].vob<<16)|angle[i].cell);
			}

			phase = P_LANGS;
		}
		else if(phase == 4)
		{
			if(!line.CompareNoCase(_T("ALL")))
			{
				for(ptrdiff_t i = 0; i < 32; i++) m_rd.selids[i] = true;
				m_rd.fClosedCaption = true;
				phase = P_OPTIONS;
			}
			else
			{
				line += ' ';

				while(line.GetLength() > 0)
				{
					int n = line.Find(_T(" "));

					CString lang = line.Left(n);

					line = line.Mid(n);
					line.TrimLeft();

					n = 0;

					int langnum;

					if(_istdigit(lang[0]))
					{
						n = _stscanf(lang, _T("%d"), &langnum);
						if(n != 1) break;

						m_rd.selids[langnum] = true;
					}
					else if(_istalpha(lang[0]))
					{
						n = _stscanf(lang, _T("%s"), langid);
						if(n != 1) break;

						int id = (langid[0] << 8) + langid[1];

						if(id == 'cc')
						{
							m_rd.fClosedCaption = true;
						}
						else
						{
							m_rd.selids[id] = true;
						}
					}
					else break;

					if(n != 1) break;
				}

				if((m_rd.selids.GetCount() > 0 || m_rd.fClosedCaption) && line.IsEmpty())
					phase = P_OPTIONS;
			}
		}
		else if(phase == 5 && !line.CompareNoCase(_T("CLOSE")))
			m_rd.fClose = true;
		else if(phase == 5 && !line.CompareNoCase(_T("BEEP")))
			m_rd.fBeep = true;
		else if(phase == 5 && !line.CompareNoCase(_T("RESETTIME")))
			m_rd.fResetTime = true;
		else if(phase == 5 && !line.CompareNoCase(_T("FORCEDONLY")))
			m_rd.fForcedOnly = true;
		else if(phase == 5 && !line.CompareNoCase(_T("CLOSEIGNOREERRORS")))
			m_rd.fCloseIgnoreError = true;

	}

	m_rd.fAuto = true;

	return phase == P_OPTIONS ? S_OK : E_FAIL;
}

STDMETHODIMP CVobSubFileRipper::SetInput(CString infn)
{
	CAutoLock cAutoLock(&m_csAccessLock);

	m_rd.Reset();

	if(!LoadIfo(infn) || !LoadVob(infn))
		return E_INVALIDARG;

	m_infn = infn;

	return S_OK;
}

STDMETHODIMP CVobSubFileRipper::SetOutput(CString outfn)
{
	CAutoLock cAutoLock(&m_csAccessLock);
	m_outfn = outfn;
	return S_OK;
}

STDMETHODIMP CVobSubFileRipper::GetRipperData(VSFRipperData& rd)
{
	CAutoLock cAutoLock(&m_csAccessLock);
	rd.Copy(m_rd);
	return S_OK;
}

STDMETHODIMP CVobSubFileRipper::UpdateRipperData(VSFRipperData& rd)
{
	CAutoLock cAutoLock(&m_csAccessLock);
	m_rd.Copy(rd);
	return S_OK;
}

STDMETHODIMP CVobSubFileRipper::Index()
{
	if(m_fIndexing) return E_FAIL;
	CAMThread::CallWorker(CMD_INDEX);
	return S_OK;
}


STDMETHODIMP CVobSubFileRipper::IsIndexing()
{
	return m_fIndexing ? S_OK : S_FALSE;
}

STDMETHODIMP CVobSubFileRipper::Abort(bool fSavePartial)
{
	m_fBreakThread = true;
	return S_OK;
}

//

void VSFRipperData::Reset()
{
	vidsize.SetSize(0,0);
	memset(&vidinfo, 0, sizeof(vidinfo));
	pgcs.RemoveAll();
	iSelPGC = -1;
	fResetTime = fClosedCaption = true;
	fForcedOnly = false;
	fClose = fBeep = fAuto = false;
	fCloseIgnoreError = false;

	selvcs.RemoveAll();
	selids.RemoveAll();
}

void VSFRipperData::Copy(VSFRipperData& rd)
{
	Reset();

	vidsize = rd.vidsize;
	vidinfo = rd.vidinfo;
	if(int len = rd.pgcs.GetCount())
	{
		pgcs.SetCount(len);
		for(ptrdiff_t i = 0; i < len; i++)
		{
			PGC& src = rd.pgcs[i];
			PGC& dst = pgcs[i];
			dst.nAngles = src.nAngles;
			for(ptrdiff_t i = 0; i < countof(dst.angles); i++)
				dst.angles[i].Copy(src.angles[i]);
			dst.iSelAngle = src.iSelAngle;
			memcpy(dst.pal, src.pal, sizeof(src.pal));
			memcpy(dst.ids, src.ids, sizeof(src.ids));
		}
	}
	iSelPGC = rd.iSelPGC;
	fResetTime = rd.fResetTime;
	fClosedCaption = rd.fClosedCaption;
	fForcedOnly = rd.fForcedOnly;
	fClose = rd.fClose;
	fBeep = rd.fBeep;
	fAuto = rd.fAuto;
	fCloseIgnoreError = rd.fCloseIgnoreError;
	selvcs.Copy(rd.selvcs);
	POSITION pos = rd.selids.GetStartPosition();
	while(pos)
	{
		BYTE key;
		bool val;
		rd.selids.GetNextAssoc(pos, key, val);
		selids[key] = val;
	}
}

